/** * GET /_emdash/api/admin/policies/:id — one policy (id or slug), with the routes it grants * PUT /_emdash/api/admin/policies/:id — update * DELETE /_emdash/api/admin/policies/:id — delete (not built-in, not held by any role) */ import type { APIRoute } from "astro"; import { requirePerm } from "#api/authorize.js"; import { apiError, apiSuccess, handleError } from "#api/error.js"; import { isParseError, parseBody } from "#api/parse.js"; import { policyUpdateBody } from "#api/schemas.js"; import { invalidateAuthzCache } from "#auth/authz.js"; import { entriesGrantedBy, pluginCatalogEntries } from "#auth/route-catalog.js"; import { AuthzRepository } from "#db/repositories/authz.js"; import { authzErrorResponse } from "../roles/index.js"; export const prerender = false; export const GET: APIRoute = async ({ params, locals }) => { const { emdash, user } = locals; if (!emdash?.db) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500); const denied = requirePerm(user, "policies:read"); if (denied) return denied; if (!params.id) return apiError("MISSING_PARAM", "Policy id required", 400); try { const item = await new AuthzRepository(emdash.db).getPolicy(params.id); if (!item) return apiError("NOT_FOUND", "Policy not found", 404); return apiSuccess({ item: { ...item, routeIds: entriesGrantedBy(item.rules, pluginCatalogEntries(emdash.listPluginRoutes?.() ?? [])).map((e) => e.id) }, }); } catch (error) { return handleError(error, "Failed to get policy", "POLICY_GET_ERROR"); } }; export const PUT: APIRoute = async ({ params, request, locals }) => { const { emdash, user, authz } = locals; if (!emdash?.db) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500); const denied = requirePerm(user, "policies:manage"); if (denied) return denied; if (!params.id) return apiError("MISSING_PARAM", "Policy id required", 400); try { const body = await parseBody(request, policyUpdateBody); if (isParseError(body)) return body; const repo = new AuthzRepository(emdash.db); const target = await repo.getPolicy(params.id); if (!target) return apiError("NOT_FOUND", "Policy not found", 404); // Rewriting a policy your own role holds can lock you out just like // editing your own role. Same rule: another administrator does it. if (body.rules !== undefined && authz?.rolePolicies.includes(target.slug)) { return apiError( "SELF_ROLE_CHANGE", "You cannot change the rules of a policy your own role holds", 400, ); } const item = await repo.updatePolicy(target.id, body); invalidateAuthzCache(); return apiSuccess({ item: { ...item, routeIds: entriesGrantedBy(item.rules, pluginCatalogEntries(emdash.listPluginRoutes?.() ?? [])).map((e) => e.id) }, }); } catch (error) { return ( authzErrorResponse(error) ?? handleError(error, "Failed to update policy", "POLICY_UPDATE_ERROR") ); } }; export const DELETE: APIRoute = async ({ params, locals }) => { const { emdash, user } = locals; if (!emdash?.db) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500); const denied = requirePerm(user, "policies:manage"); if (denied) return denied; if (!params.id) return apiError("MISSING_PARAM", "Policy id required", 400); try { await new AuthzRepository(emdash.db).deletePolicy(params.id); invalidateAuthzCache(); return apiSuccess({ success: true }); } catch (error) { return ( authzErrorResponse(error) ?? handleError(error, "Failed to delete policy", "POLICY_DELETE_ERROR") ); } };